<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <title></title>
        <style type="text/css">
            input[type="text"] { width: 300px; }
            .muted {color: #CCCCCC; font-size: 10px;}
        </style>
        <script type="text/javascript" src="https://cdn.jsdelivr.net/gh/centrifugal/centrifuge-js@master/dist/centrifuge.min.js"></script>
        <script type="text/javascript">
            // helper functions to work with escaping html.
            const tagsToReplace = {'&': '&amp;', '<': '&lt;', '>': '&gt;'};
            function replaceTag(tag) {return tagsToReplace[tag] || tag;}
            function safeTagsReplace(str) {return str.replace(/[&<>]/g, replaceTag);}

            const channel = "chat:index";

            // Manually keep last seen stream position.
            let streamPosition = null;

            window.addEventListener('load', function() {
                const input = document.getElementById('input');
                const container = document.getElementById('messages');

                const centrifuge = new Centrifuge('ws://localhost:8000/connection/websocket');

                centrifuge.on('connect', function(ctx){
                    drawText('Connected with client ID ' + ctx.client + ' over ' + ctx.transport + ' with data: ' + JSON.stringify(ctx.data));
                    input.removeAttribute("disabled");
                });

                centrifuge.on('disconnect', function(ctx){
                    drawText('Disconnected: ' + ctx.reason + (ctx.reconnect?", will try to reconnect":", won't try to reconnect"));
                    input.setAttribute("disabled", 'true');
                });

                const sub = centrifuge.subscribe(channel, handleMessage)
                        .on("unsubscribe", handleUnsubscribe)
                        .on("subscribe", handleSubscribe)
                        .on("error", handleSubscribeError);

                centrifuge.connect();

                async function restoreMissedPublications() {
                    let recovered = false;
                    while (true) {
                        if (recovered) {
                            break;
                        }
                        let resp;
                        try {
                            const limit = 10;
                            drawText("Since offset " + streamPosition.offset + " with limit " + limit)
                            resp = await sub.history({since: streamPosition, limit: limit});
                        } catch (e) {
                            return false;
                        }
                        const pubs = resp.publications;
                        if (pubs && pubs.length > 0) {
                            for (let i in pubs) {
                                if (!pubs.hasOwnProperty(i)) {
                                    continue
                                }
                                handleMessage(pubs[i]);
                                if (pubs[i].offset === resp.offset) {
                                    recovered = true;
                                    break;
                                }
                            }
                        } else {
                            recovered = true;
                            break;
                        }
                    }
                    return recovered;
                }

                async function handleSubscribe(ctx) {
                    drawText('Subscribed on channel ' + ctx.channel + ' (resubscribed: ' + ctx.isResubscribe + ')');
                    if (streamPosition == null) {
                        streamPosition = ctx.streamPosition;
                    } else {
                        drawText("Start missed publications recovery")
                        const ok = await restoreMissedPublications();
                        if (!ok) {
                            drawText("Oops, seems like we can't recover");
                            centrifuge.disconnect();
                            return;
                        }
                        drawText("Successfully recovered")
                    }
                }

                function handleSubscribeError(err) {
                    drawText('Error subscribing on channel ' + err.channel + ': ' + err.message);
                }

                function handleMessage(message) {
                    if (message.offset !== streamPosition.offset + 1) {
                        return;
                    }
                    drawMessage(message)
                    streamPosition.offset = message.offset;
                }

                function drawMessage(message) {
                    let clientID;
                    if (message.info){
                        clientID = message.info.client;
                    } else {
                        clientID = null;
                    }
                    const inputText = message.data["input"].toString();
                    const text = safeTagsReplace(inputText) + ' <span class="muted">from ' + clientID + '</span>';
                    drawText(text);
                }

                function handleUnsubscribe(sub) {
                    drawText('Unsubscribed from channel ' + sub.channel);
                }

                function drawText(text) {
                    let e = document.createElement('li');
                    e.innerHTML = [(new Date()).toString(), ' ' + text].join(':');
                    container.insertBefore(e, container.firstChild);
                }

                document.getElementById('form').addEventListener('submit', function(event) {
                    event.preventDefault();
                    sub.publish({"input": input.value}).then(function() {
                        drawText("Successfully published to channel");
                    }, function(err) {
                        drawText("Publish error: " + JSON.stringify(err));
                    });
                    input.value = '';
                });
            });
        </script>
    </head>
    <body>
        <form id="form">
            <label for="input"></label><input type="text" id="input" autocomplete="off" />
            <input type="submit" id="submit" value="»">
        </form>
        <ul id="messages"></ul>
    </body>
</html>
